/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import { TokenQueue } from '../parser/TokenQueue';
import { Evaluator } from './Evaluator';
import List from '@ohos.util.List';
import { Validate } from '../helper/Validate';
import {
  EvaluatorRoot,
  StructuralHas,
  StructuralImmediateParent,
  StructuralImmediatePreviousSibling,
  StructuralNot,
  StructuralParent,
  StructuralPreviousSibling
} from './StructuralEvaluator';
import { CombiningEvaluatorAnd, CombiningEvaluatorOr } from './CombiningEvaluator';
import { StringUtil } from '../internal/StringUtil';
import { Normalizer } from '../internal/Normalizer';
import { EvaluatorIndexLessThan } from './evaluator/EvaluatorIndexLessThan';
import { EvaluatorAllElements } from './evaluator/EvaluatorAllElements';
import { EvaluatorIndexGreaterThan } from './evaluator/EvaluatorIndexGreaterThan';
import { EvaluatorIndexEquals } from './evaluator/EvaluatorIndexEquals';
import { EvaluatorIsFirstChild } from './evaluator/EvaluatorIsFirstChild';
import { EvaluatorIsLastChild } from './evaluator/EvaluatorIsLastChild';
import { EvaluatorIsFirstOfType } from './evaluator/EvaluatorIsFirstOfType';
import { EvaluatorIsLastOfType } from './evaluator/EvaluatorIsLastOfType';
import { EvaluatorIsOnlyChild } from './evaluator/EvaluatorIsOnlyChild';
import { EvaluatorIsOnlyOfType } from './evaluator/EvaluatorIsOnlyOfType';
import { EvaluatorIsEmpty } from './evaluator/EvaluatorIsEmpty';
import { EvaluatorIsRoot } from './evaluator/EvaluatorIsRoot';
import { EvaluatorMatchText } from './evaluator/EvaluatorMatchText';
import { EvaluatorId } from './evaluator/EvaluatorId';
import { EvaluatorClass } from './evaluator/EvaluatorClass';
import { EvaluatorTag } from './Evaluator/EvaluatorTag';
import { EvaluatorTagEndsWith } from './evaluator/EvaluatorTagEndsWith';
import { EvaluatorAttributeStarting } from './evaluator/EvaluatorAttributeStarting';
import { EvaluatorAttribute } from './evaluator/EvaluatorAttribute';
import { EvaluatorAttributeWithValue } from './evaluator/EvaluatorAttributeWithValue';
import { EvaluatorAttributeWithValueNot } from './evaluator/EvaluatorAttributeWithValueNot';
import { EvaluatorAttributeWithValueStarting } from './evaluator/EvaluatorAttributeWithValueStarting';
import { EvaluatorAttributeWithValueEnding } from './evaluator/EvaluatorAttributeWithValueEnding';
import { EvaluatorAttributeWithValueContaining } from './evaluator/EvaluatorAttributeWithValueContaining';
import { EvaluatorAttributeWithValueMatching } from './evaluator/EvaluatorAttributeWithValueMatching';
import { EvaluatorIsNthChild } from './evaluator/EvaluatorIsNthChild';
import { EvaluatorIsNthLastChild } from './evaluator/EvaluatorIsNthLastChild';
import { EvaluatorContainsOwnText } from './evaluator/EvaluatorContainsOwnText';
import { EvaluatorContainsText } from './evaluator/EvaluatorContainsText';
import { EvaluatorContainsWholeOwnText } from './evaluator/EvaluatorContainsWholeOwnText';
import { EvaluatorContainsWholeText } from './evaluator/EvaluatorContainsWholeText';
import { EvaluatorContainsData } from './evaluator/EvaluatorContainsData';
import { EvaluatorMatchesOwn } from './evaluator/EvaluatorMatchesOwn';
import { EvaluatorMatchesWholeText } from './evaluator/EvaluatorMatchesWholeText';
import { EvaluatorMatches } from './evaluator/EvaluatorMatches';
import { EvaluatorMatchesWholeOwnText } from './evaluator/EvaluatorMatchesWholeOwnText';
import { EvaluatorIsNthLastOfType } from './evaluator/EvaluatorIsNthLastOfType';
import { EvaluatorIsNthOfType } from './evaluator/EvaluatorIsNthOfType';
import hilog from '@ohos.hilog';
import logUtil from '../util/LogUtil';

/**
 * Parses a CSS selector into an Evaluator tree.
 */
export class QueryParser {
  private static readonly _Combinators = [',', '>', '+', '~', ' ']
  private static readonly _AttributeEvals = ['=', '!=', '^=', '$=', '*=', '~=']
  private readonly _tq: TokenQueue
  private readonly _query: string
  private readonly evals = new List<Evaluator>()

  private constructor(query: string) {
    Validate.notEmpty(query,'QueryParser query must not be empty')
    query = query.trim()
    this._query = query
    this._tq = new TokenQueue(query)
  }

  /**
   * Parse a CSS query into an Evaluator.
   * @param query CSS query
   * @return Evaluator
   * @see Selector selector query syntax
   */
  public static parse(query: string): Evaluator {
    try {
      let p = new QueryParser(query);
      return p.parse();
    } catch (e) {
      hilog.info(0x0000, 'testTag', '%{public}s', `selectFirst parse`);
      throw new Error(e.getMessage());
    }
  }

  /**
   * Parse the query
   * @return Evaluator
   */
  parse(): Evaluator {
    this._tq.consumeWhitespace()
    logUtil.i("fredzz",`QueryParser parse:${this._tq.getQueue()}`)
    if (this._tq.matchesAny(...QueryParser._Combinators)) {
      // if starts with a combinator, use root as elements
      this.evals.add(new EvaluatorRoot())
      this.combinator(this._tq.getConsume())
    } else {
      this.evals.add(this.consumeEvaluator())
    }
    while (!this._tq.isEmpty()) {
      // hierarchy and extras
      let seenWhite = this._tq.consumeWhitespace()
      if (this._tq.matchesAny(...QueryParser._Combinators)) {
        this.combinator(this._tq.getConsume())
      } else if (seenWhite) {
        this.combinator(' ')
      } else {
        // E.class, E#id, E[attr] etc. AND
        this.evals.add(this.consumeEvaluator())
      }
    }
    logUtil.i("fredzzz",`QueryParser parse:${this.evals.length}`)
    if (this.evals.length === 1) {
      return this.evals.get(0)
    }

    return new CombiningEvaluatorAnd(/*this.evals*/)
  }

  private combinator(combinator: string) {
    this._tq.consumeWhitespace()
    // support multi > childs
    let subQuery = this.consumeSubQuery()
    // the new topmost evaluator
    let rootEval: Evaluator
    // the evaluator the new eval will be combined to. could be root, or rightmost or.
    let currentEval: Evaluator
    // the evaluator to add into target evaluator
    let newEval = QueryParser.parse(subQuery);
    let replaceRightMost = false
    if (this.evals.length === 1) {
      rootEval = currentEval = this.evals.get(0)
      // make sure OR (,) has precedence:
      if (rootEval instanceof CombiningEvaluatorOr && combinator !== ',') {
        currentEval = (currentEval as CombiningEvaluatorOr).rightMostEvaluator()
        if (currentEval === undefined || currentEval === null) {
          throw new Error(`currentEval is null`)
        }
        replaceRightMost = true
      }
    } else {
      rootEval = currentEval = new CombiningEvaluatorAnd(...this.evals)
    }
    this.evals.clear()
    // for most combinators: change the current eval into an AND of the current eval and the new eval
    switch (combinator) {
      case '>':
        currentEval = new CombiningEvaluatorAnd(new StructuralImmediateParent(currentEval), newEval);
        break;
      case ' ':
        currentEval = new CombiningEvaluatorAnd(new StructuralParent(currentEval), newEval);
        break;
      case '+':
        currentEval = new CombiningEvaluatorAnd(new StructuralImmediatePreviousSibling(currentEval), newEval);
        break;
      case '~':
        currentEval = new CombiningEvaluatorAnd(new StructuralPreviousSibling(currentEval), newEval);
        break;
      case ',':
        let or;
        if (currentEval instanceof CombiningEvaluatorOr) {
          or = currentEval as CombiningEvaluatorOr;
        } else {
          or = new CombiningEvaluatorOr();
          or.add(currentEval);
        }
        or.add(newEval);
        currentEval = or;
        break;
      default:
        throw new Error(`Unknown combinator '${combinator}'`);
    }
    if (replaceRightMost) {
      (rootEval as CombiningEvaluatorOr).replaceRightMostEvaluator(currentEval)
    } else {
      rootEval = currentEval
    }
    this.evals.add(rootEval)
  }

  private consumeSubQuery(): string {
    let sq = StringUtil.borrowBuilder()
    while (!this._tq.isEmpty()) {
      if (this._tq.matches('(')) {
        sq.push('(', this._tq.chompBalanced('(', ')'), ')')
      } else if (this._tq.matches('[')) {
        sq.push('[', this._tq.chompBalanced('[', ']'), ']')
      } else if (this._tq.matchesAny(...QueryParser._Combinators)) {
        if (sq.length > 0) {
          break
        } else {
          this._tq.getConsume()
        }
      } else {
        sq.push(this._tq.getConsume())
      }
    }
    return StringUtil.releaseBuilder(sq)
  }

  private consumeEvaluator(): Evaluator {
    if (this._tq.matchChomp('#'))
      return this.byId();
    else if (this._tq.matchChomp('.'))
      return this.byClass();
    else if (this._tq.matchesWord() || this._tq.matches('*|'))
      return this.byTag();
    else if (this._tq.matches('['))
      return this.byAttribute();
    else if (this._tq.matchChomp('*'))
      return new EvaluatorAllElements();
    else if (this._tq.matchChomp(':'))
      return this.parsePseudoSelector();
    else // unhandled
      throw new Error(`Could not parse query '${this._query}': unexpected token at '${this._tq.remainder()}'`);
  }

  private parsePseudoSelector(): Evaluator {
    let pseudo = this._tq.consumeCssIdentifier();
    switch (pseudo) {
      case 'lt':
        return new EvaluatorIndexLessThan(this.consumeIndex())
      case 'gt':
        return new EvaluatorIndexGreaterThan(this.consumeIndex())
      case 'eq':
        return new EvaluatorIndexEquals(this.consumeIndex())
      case 'has':
        return this.has()
      case 'contains':
        return this.contains(false)
      case 'containsOwn':
        return this.contains(true)
      case 'containsWholeText':
        return this.containsWholeText(false)
      case 'containsWholeOwnText':
        return this.containsWholeText(true)
      case 'containsData':
        return this.containsData()
      case 'matches':
        return this.matches(false)
      case 'matchesOwn':
        return this.matches(true)
      case 'matchesWholeText':
        return this.matchesWholeText(false)
      case 'matchesWholeOwnText':
        return this.matchesWholeText(true)
      case 'not':
        return this.not()
      case 'nth-child':
        return this.cssNthChild(false, false)
      case 'nth-last-child':
        return this.cssNthChild(true, false)
      case 'nth-of-type':
        return this.cssNthChild(false, true)
      case 'nth-last-of-type':
        return this.cssNthChild(true, true)
      case 'first-child':
        return new EvaluatorIsFirstChild()
      case 'last-child':
        return new EvaluatorIsLastChild()
      case 'first-of-type':
        return new EvaluatorIsFirstOfType()
      case 'last-of-type':
        return new EvaluatorIsLastOfType()
      case 'only-child':
        return new EvaluatorIsOnlyChild()
      case 'only-of-type':
        return new EvaluatorIsOnlyOfType()
      case 'empty':
        return new EvaluatorIsEmpty()
      case 'root':
        return new EvaluatorIsRoot()
      case 'matchText':
        return new EvaluatorMatchText()
      default:
        throw new Error(`Could not parse query '${this._query}': unexpected token at '${this._tq.remainder()}'`)
    }
  }

  private byId(): Evaluator {
    let id = this._tq.consumeCssIdentifier()
    Validate.notEmpty(id,'QueryParser byId id must not be empty')
    return new EvaluatorId(id)
  }

  private byClass(): Evaluator {
    let className = this._tq.consumeCssIdentifier()
    Validate.notEmpty(className,'QueryParser byClass className must not be empty')
    return new EvaluatorClass(className.trim())
  }

  private byTag(): Evaluator {
    // todo - these aren't dealing perfectly with case sensitivity. For case sensitive parsers, we should also make
    // the tag in the selector case-sensitive (and also attribute names). But for now, normalize (lower-case) for
    // consistency - both the selector and the element tag
    let tagName = Normalizer.normalize(this._tq.consumeElementSelector())
    logUtil.i("fredzz",`QueryParser byTag:${this._tq.getQueue()}`)
    Validate.notEmpty(tagName,'QueryParser byTag tagName must not be empty')
    // namespaces: wildcard match equals(tagName) or ending in ":"+tagName
    if (tagName.startsWith('*|')) {
      let plainTag = tagName.substring(2); // strip *|
      return new CombiningEvaluatorOr(new EvaluatorTag(plainTag),
        new EvaluatorTagEndsWith(tagName.replace('*|', ':')));
    } else {
      // namespaces: if element name is "abc:def", selector must be "abc|def", so flip:
      if (tagName.indexOf('|') >= 0) {
        tagName = tagName.replace('|', ':');
      }
      return new EvaluatorTag(tagName);
    }
  }

  private byAttribute(): Evaluator {
    // content queue
    let cq = new TokenQueue(this._tq.chompBalanced('[', ']'))
    // eq, not, start, end, contain, match, (no val)
    let key = cq.consumeToAny(...QueryParser._AttributeEvals)
    Validate.notEmpty(key,'QueryParser byAttribute key must not be empty')
    cq.consumeWhitespace()
    if (cq.isEmpty()) {
      if (key.startsWith('^')) {
        return new EvaluatorAttributeStarting(key.substring(1));
      } else {
        return new EvaluatorAttribute(key)
      }
    } else {
      if (cq.matchChomp('=')) {
        return new EvaluatorAttributeWithValue(key, cq.remainder());
      } else if (cq.matchChomp('!=')) {
        return new EvaluatorAttributeWithValueNot(key, cq.remainder());
      } else if (cq.matchChomp('^=')) {
        return new EvaluatorAttributeWithValueStarting(key, cq.remainder());
      } else if (cq.matchChomp('$=')) {
        return new EvaluatorAttributeWithValueEnding(key, cq.remainder());
      } else if (cq.matchChomp('*=')) {
        return new EvaluatorAttributeWithValueContaining(key, cq.remainder());
      } else if (cq.matchChomp('~=')) {
        new EvaluatorAttributeWithValueMatching(key, new RegExp(cq.remainder()));
      } else {
        throw new Error(`Could not parse attribute query '${this._query}': unexpected token at '${cq.remainder()}'`)
      }
    }
  }

  private cssNthChild(backwards: boolean, ofType: boolean): Evaluator {
    let arg = Normalizer.normalize(this.consumeParens())
    let mAB = /(([+-])?(\d+)?)n(\s*([+-])?\s*\d+)?/
    let mB = /([+-])?(\d+)/
    let a: number
    let b: number
    if ('odd' === arg) {
      a = 2
      b = 1
    } else if ('even' === arg) {
      a = 2
      b = 0
    } else if (mAB.test(arg)) {
      let groups = mAB.exec(arg)
      try {
        a = groups[3] !== undefined && groups[3] !== null ? parseInt(groups[1].replace('^\+', '')) : 1
      } catch (e) {
        a = 1
      }
      try {
        b = groups[4] !== undefined && groups[4] !== null ? parseInt(groups[4].replace('^\+', '')) : 0
      } catch (e) {
        b = 0
      }
    } else if (mB.test(arg)) {
      let groups = mB.exec(arg)
      a = 0
      try {
        b = parseInt(groups[0].replace('^\+', ''))
      } catch (e) {
        b = 0
      }
    } else {
      throw new Error(`Could not parse nth-index '${arg}': unexpected format`)
    }
    if (ofType) {
      if (backwards) {
        return new EvaluatorIsNthLastOfType(a, b)
      } else {
        return new EvaluatorIsNthOfType(a, b)
      }
    } else {
      if (backwards) {
        return new EvaluatorIsNthLastChild(a, b)
      } else {
        return new EvaluatorIsNthChild(a, b)
      }
    }
  }

  private consumeParens(): string {
    return this._tq.chompBalanced('(', ')')
  }

  private consumeIndex(): number {
    let index = this.consumeParens().trim()
    Validate.isTrue(StringUtil.isNumeric(index), 'Index must be numeric')
    return parseInt(index)
  }

  // pseudo selector :has(el)
  private has(): Evaluator {
    let subQuery = this.consumeParens()
    Validate.notEmpty(subQuery, ':has(selector) sub-select must not be empty')
    return new StructuralHas(QueryParser.parse(subQuery))
  }

  // pseudo selector :contains(text), containsOwn(text)
  private contains(own: boolean): Evaluator {
    let query = own ? ':containsOwn' : ':contains'
    let searchText = TokenQueue.unescape(query)
    Validate.notEmpty(searchText, query + '(text) query must not be empty')
    return own
      ? new EvaluatorContainsOwnText(searchText)
      : new EvaluatorContainsText(searchText)
  }

  private containsWholeText(own: boolean): Evaluator {
    let query = own ? ':containsWholeOwnText' : ':containsWholeText'
    let searchText = TokenQueue.unescape(this.consumeParens())
    Validate.notEmpty(searchText, query + '(text) query must not be empty')
    return own
      ? new EvaluatorContainsWholeOwnText(searchText)
      : new EvaluatorContainsWholeText(searchText)
  }

  // pseudo selector :containsData(data)
  private containsData(): Evaluator {
    let searchText = TokenQueue.unescape(this.consumeParens())
    Validate.notEmpty(searchText, ':containsData(text) query must not be empty')
    return new EvaluatorContainsData(searchText)
  }

  // :matches(regex), matchesOwn(regex)
  private matches(own: boolean): Evaluator {
    let query = own ? ':matchesOwn' : ':matches'
    let regex = this.consumeParens() // don't unescape, as regex bits will be escaped
    Validate.notEmpty(regex, query + '(regex) query must not be empty')
    return own ? new EvaluatorMatchesOwn(new RegExp(regex)) : new EvaluatorMatches(new RegExp(regex))
  }

  // :matches(regex), matchesOwn(regex)
  private matchesWholeText(own: boolean): Evaluator {
    let query = own ? ':matchesWholeOwnText' : ':matchesWholeText'
    let regex = this.consumeParens(); // don't unescape, as regex bits will be escaped
    Validate.notEmpty(regex, query + '(regex) query must not be empty')
    return own ? new EvaluatorMatchesWholeOwnText(new RegExp(regex)) : new EvaluatorMatchesWholeText(new RegExp(regex))
  }

  private not(): Evaluator {
    let subQuery = this.consumeParens()
    Validate.notEmpty(subQuery, ':not(selector) subselect must not be empty')
    return new StructuralNot(QueryParser.parse(subQuery))
  }

  public toString(): string {
    return this._query
  }
}
